/*
  PlayStation DOOM Password Editor
  
  Copyright 2014 James Haley

  This program is free software; you can redistribute it and/or
  modify it under the terms of the GNU General Public License
  as published by the Free Software Foundation; either version 2
  of the License, or (at your option) any later version.

  This program is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  GNU General Public License for more details.

  You should have received a copy of the GNU General Public License
  along with this program; if not, write to the Free Software
  Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA
  02111-1307, USA.
*/

//=============================================================================
//
// Data
//

QGlobals = {};

QGlobals.passwordData  = new Array();
QGlobals.passwordChar  = "bcdfghjklmnpqrstvwxyz0123456789!";

QGlobals.numWeapons = 9;
QGlobals.weaponControlForIndex = 
[ 
  "chainsaw", "fist", "pistol", "shotgun", "ssg", "chaingun", "missile",
  "plasma", "bfg"
];

QGlobals.numAmmo = 4;
QGlobals.ammoCtrlForIdx    = [ "clip",    "shell",    "misl",    "cell"    ];
QGlobals.maxAmmoCtrlForIdx = [ "maxclip", "maxshell", "maxmisl", "maxcell" ];

QGlobals.normMaxAmmo = [ 200,  50,  50, 300 ];
QGlobals.backMaxAmmo = [ 400, 100, 100, 600 ];

//=============================================================================
//
// Utils
//

QUtils = {};

QUtils.intDivide = function (dividend, divisor)
{
  return ~~(dividend / divisor);
};

QUtils.intMod = function (dividend, divisor)
{
  return ~~(dividend % divisor);
};

QUtils.getFormValue = function (controlName)
{
  return document.getElementById(controlName).value;
};

QUtils.setFormValue = function (controlName, val)
{
  document.getElementById(controlName).value = val;
};

QUtils.getChecked = function (controlName)
{
  return document.getElementById(controlName).checked;
};

QUtils.setChecked = function (controlName, check)
{
  document.getElementById(controlName).checked = check;
};

QUtils.makeBE16 = function (byte1, byte2)
{
  return ((byte1 << 8) | byte2);
};

QUtils.unmakeBE16 = function (innum)
{
  return [(innum & 0x00ff), ((innum & 0xff00) >> 8)];
};

QUtils.byteSwap = function (innum)
{
  var low  = (innum & 0x00ff) << 8;
  var high = (innum & 0xff00) >> 8;
  
  return (low | high);
};

QUtils.clearError = function ()
{
  document.getElementById("errormsg").textContent = "";
};

QUtils.showError = function ()
{
  var str = Array.prototype.slice.call(arguments).join("");
  document.getElementById("errormsg").textContent = str;
};

//=============================================================================
//
// Encoder
//

QEncoder = {};

QEncoder.encodePassword = function ()
{
  var encodePassItem = function (value, maxvalue)
  {
    var bit;
    value = value << 3;
    bit   = QUtils.intDivide(value, maxvalue);
    
    if(QUtils.intMod(value, maxvalue) !== 0)
      bit += 1;
      
    return bit;
  };
  
  var i;
  var bit;
  
  for(i = 0; i < 10; i++)
    QGlobals.passwordData[i] = 0;
  
  // map and skill  
  
  var nextmap   = QUtils.getFormValue("nextmap");
  var gameskill = QUtils.getFormValue("gameskill");
  
  QGlobals.passwordData[0] = (nextmap & 62) >> 1;
  QGlobals.passwordData[1] = ((nextmap & 1) << 4) | ((gameskill & 3) << 2);

  // backpack
  if(QUtils.getChecked("backpack"))
    QGlobals.passwordData[1] |= 2;

  // weapons
  if(QUtils.getChecked("chainsaw"))
    QGlobals.passwordData[1] |=  1;
  if(QUtils.getChecked("bfg"))
    QGlobals.passwordData[2] |= 16;
  if(QUtils.getChecked("plasma"))
    QGlobals.passwordData[2] |=  8;
  if(QUtils.getChecked("missile"))
    QGlobals.passwordData[2] |=  4;
  if(QUtils.getChecked("chaingun"))
    QGlobals.passwordData[2] |=  2;
  if(QUtils.getChecked("ssg"))
    QGlobals.passwordData[2] |=  1;
  if(QUtils.getChecked("shotgun"))
    QGlobals.passwordData[3] |= 16;
 
  // clip
  bit = encodePassItem(QUtils.getFormValue("clip"), QUtils.getFormValue("maxclip"));
  QGlobals.passwordData[3] |= (bit & 15);
 
  // shell
  bit = encodePassItem(QUtils.getFormValue("shell"), QUtils.getFormValue("maxshell"));
  QGlobals.passwordData[4] |= (bit & 15) << 1;
  
  // cell
  bit = encodePassItem(QUtils.getFormValue("cell"), QUtils.getFormValue("maxcell"));
  QGlobals.passwordData[4] |= (bit & 8) >> 3;
  QGlobals.passwordData[5] |= (bit & 7) << 2;
  
  // rocket
  bit = encodePassItem(QUtils.getFormValue("misl"), QUtils.getFormValue("maxmisl"));
  QGlobals.passwordData[5] |= (bit & 12) >> 2;
  QGlobals.passwordData[6] |= (bit &  3) << 3;
  
  // health
  bit = encodePassItem(QUtils.getFormValue("health"), 200);
  QGlobals.passwordData[6] |= (bit & 14) >> 1;
  QGlobals.passwordData[7] |= (bit &  1) << 4;
  
  // armor
  bit = encodePassItem(QUtils.getFormValue("armorpoints"), 200);
  QGlobals.passwordData[7] |= (bit & 15);
  
  // armortype
  QGlobals.passwordData[8] |= QUtils.getFormValue("armortype");
  
  // determine encryption key
  for(i = 0; i < 9; i++)
  	QGlobals.passwordData[9] ^= QGlobals.passwordData[i];
  	
  // encrypt
  for(i = 0; i < 9; i++)
    QGlobals.passwordData[i] ^= QGlobals.passwordData[9];
};

QEncoder.displayPassword = function ()
{
  var outputStr = "";
  
  for(var i = 0; i < 10; i++)
  	outputStr += QGlobals.passwordChar.charAt(QGlobals.passwordData[i]);
 
  QUtils.setFormValue("password", outputStr.toUpperCase());
};

//=============================================================================
//
// Decoder
//

QDecoder = {};

QDecoder.unFormatPassword = function ()
{
  var pw = QUtils.getFormValue("password");
  
  pw = pw.toLowerCase();     // convert to lowercase
  pw = pw.trim();            // trim superfluous whitespace
  
  // remove any intervening whitespace by splitting and reconcatenating
  var parts = pw.split(' ');
  pw = "";
  parts.forEach(function (element) { pw += element; });
  
  return pw;
};

QDecoder.validatePasswordBasic = function (pw)
{
  var i;
  
  // must be 10 characters long
  if(pw.length !== 10)
  {
    QUtils.showError("Password must contain 10 significant characters!");
    return false;
  }
  
  // validate individual characters
  for(i = 0; i < pw.length; i++)
  {
    if(QGlobals.passwordChar.indexOf(pw.charAt(i)) === -1)
    {
      QUtils.showError("Password contains an invalid character! Valid chars are: " + QGlobals.passwordChar);
      return false;
    }
  }

  // transform string back into passwordData array
  for(i = 0; i < 10; i++)
    QGlobals.passwordData[i] = QGlobals.passwordChar.indexOf(pw.charAt(i));
  
  // basic validation is successful
  return true;
};

QDecoder.decodePassword = function ()
{
  var decodePassItem = function (bytecode, maxvalue)
  {
    var value;
    var bitsum = bytecode * maxvalue;
    
    if(bitsum >= 0)
      value = bitsum >> 3;
    else
      value = (bitsum + 7) >> 3;
      
    return value;
  };
  
  var i;
  var temp = new Array();
  
  // make temp copy of password
  for(i = 0; i < 10; i++)
    temp[i] = QGlobals.passwordData[i];
  
  // verify password hash
  var encryptionKey = temp[9];
  var computedKey = 0;
  for(i = 0; i < 9; i++)
  {
  	temp[i] ^= encryptionKey;
  	computedKey ^= temp[i];
  }
  
  if(computedKey !== encryptionKey)
  {
  	QUtils.showError("Password does not hash!");
  	return false;
  }
  
  // check "danger" bits
  if(temp[8] & 28)
  {
  	QUtils.showError("Invalid password!");
  	return false;
  }
  
  // get map
  var gamemap = (temp[0] << 1) | ((temp[1] & 16) >> 4);
  if(gamemap <= 0 || gamemap > 59)
  {
  	QUtils.showError("Invalid destination level!");
  	return false;
  }
  QUtils.setFormValue("nextmap", gamemap);
  
  // get skill
  var skill = (temp[1] & 12) >> 2;
  if(skill < 0 || skill > 3)
  {
  	QUtils.showError("Invalid skill level!");
  	return false;
  }
  QUtils.setFormValue("gameskill", skill);
  
  
  // backpack
  var backpack = (temp[1] & 2) >> 1;
  if(backpack !== 0)
  {
    QUtils.setChecked("backpack", true);
    ammoToUse = QGlobals.backMaxAmmo;
  }
  else
  {
    QUtils.setChecked("backpack", false);
    ammoToUse = QGlobals.normMaxAmmo;
  }
  QForm.onClickBackpack(); // make maxammo amounts consistent  
  
  // weapons
  if(temp[1] & 1)
    QUtils.setChecked("chainsaw", true);
  else
    QUtils.setChecked("chainsaw", false);
    
  if(temp[2] & 16)
    QUtils.setChecked("bfg", true);
  else
    QUtils.setChecked("bfg", false);
    
  if(temp[2] & 8)
    QUtils.setChecked("plasma", true);
  else
    QUtils.setChecked("plasma", false);  
    
  if(temp[2] & 4)
    QUtils.setChecked("missile", true);
  else
    QUtils.setChecked("missile", false);
    
  if(temp[2] & 2)
    QUtils.setChecked("chaingun", true);
  else
    QUtils.setChecked("chaingun", false);
    
  if(temp[2] & 1)
    QUtils.setChecked("ssg", true);
  else
    QUtils.setChecked("ssg", false);
    
  if(temp[3] & 16)
    QUtils.setChecked("shotgun", true);
  else
    QUtils.setChecked("shotgun", false);
    
  // get ammo amounts
  var clip  = temp[3] & 15;
  var shell = (temp[4] & 30) >> 1;
  var cell  = ((temp[4] & 1) << 3) | ((temp[5] & 28) >> 2);
  var misl  = ((temp[5] & 3) << 2) | ((temp[6] & 24) >> 3);
  
  QUtils.setFormValue("clip",  decodePassItem(clip,  ammoToUse[0]));
  QUtils.setFormValue("shell", decodePassItem(shell, ammoToUse[1]));
  QUtils.setFormValue("misl",  decodePassItem(misl,  ammoToUse[2]));
  QUtils.setFormValue("cell",  decodePassItem(cell,  ammoToUse[3]));
  
  // get health and armor
  var health = ((temp[6] & 7) << 1) | ((temp[7] & 16) >> 4);
  var armor  = temp[7] & 15;
  
  QUtils.setFormValue("health",      decodePassItem(health, 200));
  QUtils.setFormValue("armorpoints", decodePassItem(armor,  200));

  // get armor type
  var armorclass = temp[8] & 3;
  QUtils.setFormValue("armortype", armorclass);

  return true;
};

//=============================================================================
//
// Form Maintenance
//

QForm = {};

QForm.onResetForm = function ()
{
  for(var i = 0; i < 10; i++)
    QGlobals.passwordData[i] = 0;
    
  QUtils.clearError();
};

QForm.validateControls = function ()
{
  var i;
  
  // map
  var nextmap = parseInt(QUtils.getFormValue("nextmap"));
  if(isNaN(nextmap) || nextmap < 1 || nextmap > 59)
  {
    QUtils.showError("Destination level must be between 1 and 59!");
    return false;
  }

  // max ammo amounts
  for(i = 0; i < QGlobals.numAmmo; i++)
  {
    var ammoAmt = parseInt(QUtils.getFormValue(QGlobals.ammoCtrlForIdx[i]));
    var maxAmmo = parseInt(QUtils.getFormValue(QGlobals.maxAmmoCtrlForIdx[i]));
    
    if(isNaN(ammoAmt) || ammoAmt < 0 || ammoAmt > (15*maxAmmo >> 3))
    {
      QUtils.showError("Ammo amounts must be between 0 and allowable password maximum!");
      return false;
    }
  }
  
  // health
  var health = parseInt(QUtils.getFormValue("health"));
  if(isNaN(health) || health < 0 || health > (15*200 >> 3))
  {
    QUtils.showError("Health must be between 0 and 375!");
    return false;
  }

  // armor
  var armor = parseInt(QUtils.getFormValue("armorpoints"));
  if(isNaN(armor) || armor < 0 || armor > (15*200 >> 3))
  {
    QUtils.showError("Armor must be between 0 and 375!");
    return false;
  }
  
  // everything is good!
  return true;
};
 
QForm.onEncode = function ()
{
  QUtils.clearError();
  
  if(!this.validateControls())
    return;
  
  // calculate password from selected options
  QEncoder.encodePassword();
  
  // display the password
  QEncoder.displayPassword();
};

QForm.onDecode = function ()
{
  // retrieve and unformat password from the form
  var pw = QDecoder.unFormatPassword();

  QUtils.clearError();
  
  // run basic validation checks
  if(!QDecoder.validatePasswordBasic(pw))
    return;
  
  // decode the password
  QDecoder.decodePassword();
};

QForm.onClickBackpack = function ()
{
  var i;
  var ammoToUse;
  var haveBackpack = QUtils.getChecked("backpack");
  
  if(haveBackpack)
    ammoToUse = QGlobals.backMaxAmmo;
  else
    ammoToUse = QGlobals.normMaxAmmo;
    
  for(i = 0; i < QGlobals.numAmmo; i++)
  {
    QUtils.setFormValue(QGlobals.maxAmmoCtrlForIdx[i], ammoToUse[i]);
    
    // clip corresponding ammo amount if above new threshold
    var ammo = parseInt(QUtils.getFormValue(QGlobals.ammoCtrlForIdx[i]));
    if(ammo > ammoToUse[i])
      QUtils.setFormValue(QGlobals.ammoCtrlForIdx[i], ammoToUse[i]);
  }
};

QForm.onGiveAll = function ()
{
  var i;
  
  QUtils.clearError();  
  
  // leave map and skill settings alone.
  
  // give backpack and set max ammos
  QUtils.setChecked("backpack", true);
  this.onClickBackpack();
  
  // give all weapons
  for(i = 0; i < QGlobals.numWeapons; i++)
  {
    if(i !== 1 && i !== 2) // not fist or pistol
      QUtils.setChecked(QGlobals.weaponControlForIndex[i], true);
  }
    
  // give maximum ammo for each amount
  for(i = 0; i < QGlobals.numAmmo; i++)
  {
    QUtils.setFormValue(QGlobals.ammoCtrlForIdx[i], 
                        QUtils.getFormValue(QGlobals.maxAmmoCtrlForIdx[i]));
  }
  
  // give max health
  QUtils.setFormValue("health", "200");
  
  // give max blue armor
  QUtils.setFormValue("armortype",     "2");
  QUtils.setFormValue("armorpoints", "200");
  
  // calculate password
  this.onEncode();
};

// EOF

